﻿using System;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using FLib;
using LibraryGuard.Interfaces;
using LibraryGuard.Interfaces.Configuration;

namespace LibraryGuard.Guards
{
	/// <summary>
	/// Guards tags correctness.
	/// </summary>
	public class TagsGuard
		: IGuard
	{
		#region Logger
		private static NLog.Logger Logger = NLog.LogManager.GetLogger("TagsGuard");
		#endregion

		#region Fields
		private static readonly Regex BonusTrackRegex = new Regex(@"[,\(\[]*\s*bonus\s*track\s*[,\)\]]*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
		private static readonly string[] TagsToTrim = new string[] { "Album", "AlbumArtists", "Composers", "Genres", "Performers", "Title" };
		private static readonly PropertyInfo[] PropertiesToTrim = null;

		private readonly IConfiguration BaseConfig = null;
		private readonly Configuration.TagsGuard Config = null;
		#endregion

		#region Static constructor
		static TagsGuard()
		{
			PropertiesToTrim = new PropertyInfo[TagsToTrim.Length];
			var t = typeof(TagLib.Tag);
			for (int i = 0; i < TagsToTrim.Length; i++)
			{
				PropertiesToTrim[i] = t.GetProperty(TagsToTrim[i]);
			}
		}
		#endregion

		#region IGuard Members
		/// <inheritdoc />
		public bool OnlySpecificExtensions
		{
			get { return true; }
		}

		/// <inheritdoc />
		public IActionList ActionList { get; set; }

		/// <inheritdoc />
		public void Run()
		{
			this.BaseConfig.Directories.ForEach(d => this.ActionList.Add(new GuardDirectory(d, this.BaseConfig, this.Config)));
		}

		/// <inheritdoc />
		public void FileSystemEvent(FileSystemEventArgs args, string baseDirectory)
		{
			if (args.ChangeType == WatcherChangeTypes.Changed || args.ChangeType == WatcherChangeTypes.Created || args.ChangeType == WatcherChangeTypes.Renamed)
			{
				Logger.Trace("File '{0}' changed/created/renamed, guard", args.FullPath);
				var file = new FileInfo(args.FullPath);
				this.ActionList.Remove<SaveTags>(file, Comparers.FSI);
				this.ActionList.Add(new CorrectTags(file, this.Config), Comparers.FSI);
			}
		}
		#endregion

		#region Constructor
		public TagsGuard(IConfiguration config, IGuardConfiguration guard)
		{
			Validate.Debug(() => config, v => v.NotNull());
			Validate.Debug(() => guard, v => v.NotNull());

			this.BaseConfig = config;
			this.Config = (Configuration.TagsGuard)guard;

			Logger.Info("Tags guard enabled");
			if (this.Config.Extra)
				Logger.Debug("[*] will be removed");
			if (this.Config.BonusTrack)
				Logger.Debug("'bonus track' will be removed");
			if (this.Config.Brackets)
				Logger.Debug("Brackets will be changed to parentheses");
			if (this.Config.Trim)
				Logger.Debug("Tags will be trimmed");
		}
		#endregion

		#region Actions
		/// <summary>
		/// Giards directory.
		/// </summary>
		private sealed class GuardDirectory
			: IActionInfo
		{
			#region
			private readonly DirectoryInfo Directory = null;
			private readonly IConfiguration BaseConfig = null;
			private readonly Configuration.TagsGuard Config = null;
			#endregion

			#region IActionInfo Members
			/// <inheritodc />
			public object ActionId
			{
				get { return this.Directory; }
			}

			/// <inheritdoc />
			public bool IsEdgeAction
			{
				get { return false; }
			}

			/// <inheritdoc />
			public bool Execute(IActionList actionList)
			{
				Logger.Trace("Checking directory '{0}'", this.Directory.FullName);
				foreach (var ext in this.BaseConfig.Extensions)
				{
					try
					{
						var files = this.Directory.GetFiles("*." + ext, SearchOption.AllDirectories);
						Array.ForEach(files, f => actionList.Add(new CorrectTags(f, this.Config), Comparers.FSI));
					}
					catch (Exception ex)
					{
						Logger.WarnException("Cannot search directory '{0}' for files with extension '{1}', skipping".FormatWith(this.Directory.FullName, ext), ex);
					}
				}
				return true;
			}
			#endregion

			#region Constructor
			public GuardDirectory(DirectoryInfo di, IConfiguration baseConfig, Configuration.TagsGuard config)
			{
				this.Directory = di;
				this.BaseConfig = baseConfig;
				this.Config = config;
			}
			#endregion
		}

		/// <summary>
		/// Corrects file tags.
		/// </summary>
		private sealed class CorrectTags
			: IActionInfo
		{
			#region
			private readonly FileInfo File = null;
			private readonly Configuration.TagsGuard Config = null;
			#endregion

			#region IActionInfo Members
			/// <inheritodc />
			public object ActionId
			{
				get { return this.File; }
			}

			/// <inheritdoc />
			public bool IsEdgeAction
			{
				get { return false; }
			}

			/// <inheritdoc />
			public bool Execute(IActionList actionList)
			{
				this.File.Refresh();
				if (!this.File.Exists)
				{
					Logger.Debug("File '{0}' doesn't exist, ignoring", this.File.FullName);
					return true;
				}

				Logger.Trace("Checking file '{0}'", this.File.FullName);
				TagLib.File fileTags = null;
				try
				{
					fileTags = TagLib.File.Create(this.File.FullName, TagLib.ReadStyle.None);
					bool changed = false;

					if (this.Config.Extra)
						changed = ChangeExtra(fileTags.Tag);
					if (this.Config.BonusTrack)
						changed = ChangeBonusTrack(fileTags.Tag) || changed;
					changed = TagPropertiesCorrection(fileTags.Tag, this.Config.Trim, this.Config.Brackets) || changed;

					if (changed)
					{
						actionList.Replace(new SaveTags(this.File, fileTags), Comparers.FSI);
					}
					else
					{
						Logger.Trace("File '{0}' is correct", this.File.FullName);
						fileTags.Dispose();
					}
				}
				catch (Exception ex)
				{
					if (fileTags != null)
						fileTags.Dispose();
					Logger.WarnException("Cannot correct tags in file '{0}', file is probably corrupted, ignoring".FormatWith(this.File.FullName), ex);
				}
				return true;
			}
			#endregion

			#region Constructor
			public CorrectTags(FileInfo file, Configuration.TagsGuard config)
			{
				this.File = file;
				this.Config = config;
			}
			#endregion
		}

		/// <summary>
		/// Edge action. Saves corrected tags.
		/// </summary>
		private sealed class SaveTags
			: IActionInfo
		{
			#region Private fields
			private readonly FileInfo File = null;
			private readonly TagLib.File Tags = null;
			#endregion

			#region IActionInfo Members
			/// <inheritdoc />
			public object ActionId
			{
				get { return this.File; }
			}

			/// <inheritdoc />
			public bool IsEdgeAction
			{
				get { return true; }
			}

			/// <inheritdoc />
			public bool Execute(IActionList actionList)
			{
				this.File.Refresh();
				if (!this.File.Exists)
				{
					Logger.Debug("File '{0}' doesn't exist, ignoring", this.File.FullName);
					return true;
				}

				try
				{
					this.Tags.Save();
					Logger.Info("Corrected tags in '{0}' saved", this.File.FullName);
					this.Tags.Dispose();
					this.File.LastAccessTime = DateTime.Now;
				}
				catch (Exception ex)
				{
					Logger.WarnException("Cannot save file '{0}'".FormatWith(this.File.FullName), ex);
					return false;
				}
				return true;
			}
			#endregion

			#region Constructor
			public SaveTags(FileInfo fi, TagLib.File tags)
			{
				this.File = fi;
				this.Tags = tags;
			}
			#endregion

			#region ToString override
			public override string ToString()
			{
				return "Save corrected tags to '{0}'".FormatWith(this.File.FullName);
			}
			#endregion
		}
		#endregion

		#region Auxiliary methods
		private static bool ChangeExtra(TagLib.Tag tags)
		{
			int original = tags.Title.Length;
			tags.Title = tags.Title.Replace("[*]", "").Replace("(*)", "").Replace("()", "").Replace("[]", "");
			return tags.Title.Length != original;
		}

		private static bool ChangeBonusTrack(TagLib.Tag tags)
		{
			bool changed = false;
			Match match = null;
			while ((match = BonusTrackRegex.Match(tags.Title)).Success)
			{
				tags.Title = tags.Title.Remove(match.Index, match.Length);
				changed = true;
			}
			return changed;
		}

		/// <summary>
		/// Looks shitty, but works.
		/// </summary>
		/// <param name="tags"></param>
		/// <returns></returns>
		private static bool TagPropertiesCorrection(TagLib.Tag tags, bool trim, bool brackets)
		{
			bool changed = false;
			foreach (var prop in PropertiesToTrim)
			{
				var val = prop.GetValue(tags, null);
				if (val is string)
				{
					var str = (string)val;
					CorrectTag(ref str, trim, brackets, ref changed);
					prop.SetValue(tags, str, null);
				}
				else
				{
					var strs = (string[])val;
					for (int i = 0; i < strs.Length; i++)
					{
						CorrectTag(ref strs[i], trim, brackets, ref changed);
					}
					prop.SetValue(tags, strs, null);
				}
			}
			return changed;
		}

		private static void CorrectTag(ref string tag, bool trim, bool brackets, ref bool changed)
		{
			if (trim)
			{
				int original = tag.Length;
				tag = tag.Trim();
				changed = tag.Length != original || changed;
			}
			if (brackets)
			{
				changed = tag.Contains("[") || tag.Contains("[") || changed;
				tag = tag.Replace('[', '(').Replace(']', ')');
			}
		}
		#endregion
	}
}
